﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using BuildStatus.Web.Models;
using System.Net.Http;
using System.Net;
using Newtonsoft.Json.Linq;
using BuildStatus.Web.Properties;
using System.Text;
using System.IO;

namespace BuildStatus.Web.BuildServer
{
    // https://www.visualstudio.com/en-us/docs/integrate/api/tfs/projects
    // https://www.visualstudio.com/en-us/docs/integrate/api/xamlbuild/builds
    public class BuildServerRestAPI : IDisposable, IBuildStatusServer
    {
        private string _apiVersion = "2.0";
        private const string QUOTE = "\"";
        private const string RELEASEDQUOTE = "\\\"";
        internal const string DateTimeFormat = "yyyy.MM.dd HH:mm:ss";
        internal const string TfsDateTimeFormat = "yyyy-MM-dd\\THH:mm:ss\\Z";

        private DateTime _previousBuildDefinitionsLastCleared = DateTime.Now;
        private Dictionary<string, List<int>> _previousBuildDefinitionIDs = new Dictionary<string, List<int>>();

        private DateTime _lastCheckTime;
        private IEnumerable<BuildProject> _lastBuildProjects;

        public BuildServerRestAPI()
        {
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
            }
        }

        private string GetTeamProjectURL(dynamic teamProject)
        {
            return $"{BuildSettings.Settings.TFSUri}/{teamProject.name}/";
        }

        private string GetTaskboardURL(dynamic teamProject)
        {
            return GetTeamProjectURL(teamProject) + "_backlogs/TaskBoard/";
        }

        private string GetBuildsURL(dynamic teamProject)
        {
            return GetTeamProjectURL(teamProject) + "_build";
        }

        public IEnumerable<BuildProject> GetBuildProjects()
        {
            List<BuildProject> result = new List<BuildProject>();

            if (DateTime.Now >= _previousBuildDefinitionsLastCleared.AddHours(1))
            {
                _previousBuildDefinitionIDs.Clear();
                _previousBuildDefinitionsLastCleared = DateTime.Now;

                _lastBuildProjects = null;
            }

            DateTime minFinishDate = DateTime.Now.AddDays(-Settings.Default.MaxQueryDays);
            string minFinishTime = minFinishDate.ToString(TfsDateTimeFormat);

            List<dynamic> teamProjects = QueryTfsList("value", "_apis/projects").OrderBy(x => x.name).ToList();
            foreach (dynamic teamProject in teamProjects)
            {
                BuildProject buildProject = new BuildProject()
                {
                    Name = teamProject.name,
                    CssName = ((string)teamProject.name).Replace(" ", ""),
                    TaskboardURL = GetTaskboardURL(teamProject),
                    BuildsURL = GetBuildsURL(teamProject)
                };


                string tfsquery = $"{teamProject.name}/_apis/build/builds?minFinishTime={minFinishTime}";

                List<int> definitionIds;
                if (_lastBuildProjects != null)
                {
                    // tfsquery = $"{teamProject.name}/_apis/build/builds?minFinishTime={_lastCheckTime.ToString(TfsDateTimeFormat)}";
                }

                if (_previousBuildDefinitionIDs.TryGetValue(buildProject.Name, out definitionIds))
                {
                    tfsquery += "&definitions=" + string.Join(",", definitionIds.Distinct());
                }

                List<BuildDefinition> buildDefinitions = new List<BuildDefinition>();
                List<dynamic> builds = QueryTfsList("value", tfsquery);
                foreach (dynamic build in builds)
                {
                    DateTime? finishTime = (build.finishTime != null) ? (DateTime?)((DateTime)build.finishTime).ToLocalTime() : null;
                    if (finishTime != null && finishTime.Value < minFinishDate)
                        continue;

                    if (finishTime == null)
                        finishTime = DateTime.Now;

                    string statusStr = "";
                    try
                    {
                        statusStr = build.result.Value;
                    }
                    catch
                    {
                        try
                        {
                            statusStr = build.status.Value;
                        }
                        catch (Exception ex)
                        {
                        }
                    }

                    string buildStatus = GetBuildStatus(statusStr);

                    try
                    {
                        BuildReference buildReference = new BuildReference((string)build.definition.type, (string)teamProject.name, (string)build.definition.name, (string)build.id);

                        DateTime buildStartTime = build.startTime ?? build.queueTime ?? null;

                        string buildVersion = "";
                        if (build.tags != null)
                        {
                            try
                            {
                                foreach (string tag in build.tags)
                                {
                                    if (tag.StartsWith("version=", StringComparison.CurrentCultureIgnoreCase))
                                    {
                                        buildVersion = tag.Substring(8).Trim();
                                        break;
                                    }
                                }
                            }
                            catch
                            {
                            }
                        }

                        string buildName = (string)build.definition.name;
                        if (!string.IsNullOrEmpty(buildVersion))
                        {
                            int lastIdx = buildVersion.LastIndexOf('.');

                            string newVersion = buildVersion;
                            if (lastIdx > 0 && !buildName.Contains(buildVersion.Substring(0, lastIdx)))
                            {
                                Version newVer = Version.Parse(buildVersion);
                                if (newVer.Build > 0)
                                    newVersion = $"{newVer.Major}.{newVer.Minor}.{newVer.Build.ToString().PadLeft(2, '0')}.{newVer.Revision}";

                                lastIdx = newVersion.LastIndexOf('.');
                            }

                            if (lastIdx > 0 && buildName.Contains(newVersion.Substring(0, lastIdx)))
                            {
                                string oldVer = newVersion.Substring(0, lastIdx);
                                buildName = buildName.Replace(oldVer, newVersion);
                            }
                            else
                            {
                                buildName += " " + newVersion;
                            }
                        }

                        BuildDefinition buildDefinition = new BuildDefinition()
                        {
                            Name = buildName,
                            BuildDefinitionId = build.definition.id,
                            DisplayClass = buildStatus,
                            Status = buildStatus,
                            RequestedBy = (string)build.requestedFor.displayName,
                            QueueTime = build.queueTime,
                            StartTime = buildStartTime != null ? buildStartTime.ToLocalTime().ToString(DateTimeFormat) : "",
                            Duration = build.startTime != null ? (finishTime.Value - ((DateTime)build.startTime).ToLocalTime()).ToString(@"hh\:mm\:ss") : "",
                            BuildNumber = buildReference.ToString(),
                            Version = buildVersion,
                            IsWaiting = (buildStatus == "NotStarted"),
                            Progress = 0,
                        };

                        buildDefinitions.Add(buildDefinition);
                    }
                    catch (Exception ex)
                    {
                    }
                }


                List<TimeSpan> buildTimes = new List<TimeSpan>();
                foreach (dynamic build in builds.Where(x => x.finishTime != null).OrderByDescending(x => (DateTime)x.finishTime))
                {
                    if (build.result == null || ((string)build.result).ToLower() != "succeeded")
                        continue;

                    if (build.startTime != null)
                    {
                        if (build.finishTime != null)
                        {
                            TimeSpan buildTime = ((DateTime)build.finishTime).ToLocalTime() - ((DateTime)build.startTime).ToLocalTime();
                            buildTimes.Add(buildTime);

                            if (buildTimes.Count >= 5)
                                break;
                        }
                    }
                }


                int avgSeconds = (buildTimes.Count > 0) ? (int)(buildTimes.Sum(x => x.TotalSeconds) / buildTimes.Count) : 0;
                foreach (BuildDefinition buildInProgress in buildDefinitions)
                {
                    if (buildInProgress.Status.ToLower() != "inprogress")
                        continue;

                    dynamic build = builds.FirstOrDefault(x => buildInProgress.BuildNumber.EndsWith(x.id.ToString()));
                    if (build == null || build.startTime == null)
                        continue;


                    TimeSpan duration = DateTime.Now - ((DateTime)build.startTime).ToLocalTime();

                    buildInProgress.Progress = (int)(100 * duration.TotalSeconds / avgSeconds);
                    if (buildInProgress.Progress > 100)
                        buildInProgress.Progress = 100;
                }


                foreach (int buildDefinitionId in buildDefinitions.Select(x => x.BuildDefinitionId).Distinct())
                {
                    List<BuildDefinition> currentBuilds = buildDefinitions.Where(x => x.BuildDefinitionId == buildDefinitionId).ToList();
                    currentBuilds.Sort(new BuildComparer());

                    BuildDefinition buildDefinition = currentBuilds.First();
                    buildDefinition.History.AddRange(currentBuilds.Skip(1).Take(Settings.Default.MaxHistoryBuilds));
                    buildProject.Builds.Add(buildDefinition);
                }


                BuildProject previousBuildProject = _lastBuildProjects?.FirstOrDefault(x => x.Name == buildProject.Name);
                if (previousBuildProject != null)
                {
                    foreach (BuildDefinition previousBuildDefinition in previousBuildProject.Builds)
                    {
                        if (buildProject.Builds.Count >= Settings.Default.MaxHistoryBuilds)
                            break;

                        if (!buildProject.Builds.Any(x => x.BuildNumber == previousBuildDefinition.BuildNumber))
                            buildProject.Builds.Add(previousBuildDefinition);
                    }
                }


                buildProject.Builds.Sort(new BuildDefinitionComparer());


                if (_previousBuildDefinitionIDs.ContainsKey(buildProject.Name))
                    _previousBuildDefinitionIDs.Remove(buildProject.Name);
                _previousBuildDefinitionIDs.Add(buildProject.Name, buildProject.Builds.Select(x => x.BuildDefinitionId).ToList());


                if (buildProject.Builds.Count > 0)
                    result.Add(buildProject);
            }

            //_lastBuildProjects = result;
            //_lastCheckTime = DateTime.Now;

            return result;
        }

        public BuildLog GetBuildLog(string buildNumber)
        {
            BuildReference buildReference = BuildReference.FromString(buildNumber);

            return GetVSOBuildLog(buildReference.Project, buildReference.BuildID);
        }

        public void RetryBuild(string buildNumber)
        {
            BuildReference buildReference = BuildReference.FromString(buildNumber);
            QueueBuild(buildReference.Project, buildReference.BuildDefinition, buildReference.BuildID);
        }

        private void QueueBuild(string teamProject, string buildDefinition, string originatingBuildID)
        {
            List<dynamic> definitions = QueryTfsList("value", $"{teamProject}/_apis/build/definitions?name={buildDefinition}");
            if (definitions.Count == 0)
                throw new Exception("Build definition not found.");

            int buildId = definitions.First().id;
            //buildId = 0;

            StringBuilder postData = new StringBuilder();
            postData.AppendLine("{");
            postData.AppendLine("  \"definition\": {");
            postData.AppendLine("   \"id\": " + buildId.ToString());
            postData.AppendLine("   },");
            postData.AppendLine($"   {QUOTE}parameters{QUOTE}: {QUOTE}{{ {RELEASEDQUOTE}OriginatingBuildID{RELEASEDQUOTE}: {RELEASEDQUOTE}{originatingBuildID}{RELEASEDQUOTE} }}{QUOTE}");
            postData.AppendLine("}");


            dynamic result = QueryTfs($"{teamProject}/_apis/build/builds", postData.ToString());
            if (!string.IsNullOrEmpty((string)result.message))
                throw new Exception("Queue failed with error: " + (string)result.message);
        }

        private BuildLog GetVSOBuildLog(string project, string buildId)
        {
            dynamic buildDetails = QueryTfs($"/{project}/_apis/build/builds/{buildId}");

            string statusStr = "";
            try
            {
                statusStr = buildDetails.result.Value;
            }
            catch
            {
                try
                {
                    statusStr = buildDetails.status.Value;
                }
                catch
                {
                }
            }

            BuildLog result = new BuildLog();
            result.Status = GetBuildStatus(statusStr);
            result.URL = $"{BuildSettings.Settings.TFSUri}/{project}/_build?_a=summary&buildId={buildId}";
            return result;
        }

        private string GetBuildStatus(string status)
        {
            switch ((status ?? "").ToLower())
            {
                case "completed": return "Succeeded";
                case "succeeded": return "Succeeded";
                case "partiallysucceeded": return "PartiallySucceeded";
                case "inprogress": return "InProgress";
                case "queued": return "Queued";
                case "notstarted": return "Queued"; // return "NotStarted";
                case "stopped": return "Stopped";
                case "canceled": return "Stopped";
                case "failed": return "Failed";
            }

            return status;
        }

        private List<dynamic> ConvertJObjectToList(JObject src, string propertyName)
        {
            return src[propertyName].OfType<dynamic>().ToList();
        }

        private List<dynamic> QueryTfsList(string propertyname, string uripart, string bodyContent = null)
        {
            JObject data = QueryTfs(uripart, bodyContent);
            return ConvertJObjectToList(data, propertyname);
        }

        private JObject QueryTfs(string uripart, string bodyContent = null)
        {
            using (HttpClient client = GetClient())
            {
                var url = uripart;
                if (!uripart.StartsWith(BuildSettings.Settings.TFSUri))
                {
                    url = BuildSettings.Settings.TFSUri;
                    if (!url.EndsWith("/"))
                        url += "/";

                    url += uripart; // $"_apis/projects?api-version={_apiVersion}";
                    if (!url.Contains("?")) url += "?";
                    else url += "&";

                    url += $"api-version={_apiVersion}";
                }

                using (var request = new HttpRequestMessage(string.IsNullOrEmpty(bodyContent) ? HttpMethod.Get : HttpMethod.Post, url))
                {
                    request.Headers.Add("Accept", "application/json;api-version=" + _apiVersion);

                    if (!string.IsNullOrEmpty(bodyContent))
                    {
                        request.Properties.Add("Content-Type", "application/json");
                        request.Content = new StringContent(bodyContent, Encoding.Default, "application/json");
                    }


                    using (var response = client.SendAsync(request).Result)
                    {
                        var buildsContent = response.Content.ReadAsStringAsync().Result;
                        return JObject.Parse(buildsContent);
                    }
                }
            }
        }

        private HttpClient GetClient()
        {
            HttpClientHandler handler = new HttpClientHandler();
            if (!string.IsNullOrEmpty(Settings.Default.TFSUsername))
            {
                // handler.Credentials = new NetworkCredential("username@domain", "password");
                handler.Credentials = new NetworkCredential(Settings.Default.TFSUsername, Settings.Default.TFSPassword);
            }
            else
            {
                handler.UseDefaultCredentials = true;
            }

            return new HttpClient(handler);
        }

        private static string TimeSpanToStr(TimeSpan self)
        {
            StringBuilder result = new StringBuilder();

            if (self.Days > 0)
            {
                result.Append(self.Days);
                result.Append("days ");
            }

            if (self.Hours > 0 || result.Length > 0)
            {
                result.Append(self.Hours.ToString().PadLeft(2, '0'));
                result.Append(":");
            }

            result.Append(self.Minutes.ToString().PadLeft(2, '0'));
            result.Append(":");

            result.Append(self.Seconds.ToString().PadLeft(2, '0'));

            return result.ToString().Trim();
        }

        private class BuildReference
        {
            public string BuildType { get; set; }
            public string Project { get; set; }
            public string BuildDefinition { get; set; }
            public string BuildID { get; set; }

            public BuildReference()
            {
                BuildType = "Build";
                Project = "";
                BuildID = "";
            }

            public BuildReference(string buildDefinitionType, string teamProjectName, string buildDefinition, string buildId)
            {
                BuildType = buildDefinitionType;
                Project = teamProjectName;
                BuildDefinition = buildDefinition;
                BuildID = buildId;
            }

            public static BuildReference FromString(string buildNumber)
            {
                string[] parts = buildNumber.Split('_');

                return new BuildReference(parts[0], parts[1], parts[2], buildNumber.Substring(buildNumber.LastIndexOf('_') + 1));
            }

            public override string ToString()
            {
                return $"{BuildType}_{Project}_{BuildDefinition}_{BuildID}";
            }
        }
    }
}